Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

imageCaptureProcessing maintain EXIF #2902

Open
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

melmathari
Copy link

@melmathari melmathari commented Nov 24, 2024

Images taken in CommCare were losing their EXIF metadata, including critical GPS/location data, during processing. This created headaches for users who needed that juicy metadata intact, especially for location-based verification.

Resolves issue #2689 and #2743

The Fix

We pimped the image capture and processing workflow to make EXIF data stick, no matter what. Here's what we did:

  1. Injected EXIF-aware copying for raw image files.

  2. Ensured EXIF data is kept intact during image scaling.

  3. Made sure key EXIF tags stay in the game:

    • GPS coordinates (latitude/longitude)
    • GPS timestamps
    • Image datetime
    • Orientation metadata

How We Pulled It Off

  • Created copyFileWithExifData() to keep EXIF data alive during file copying.
  • Introduced scaleAndSaveImageWithExif() for maintaining EXIF while scaling images.
  • Modified ImageCaptureProcessing to use our new EXIF-hugging methods.
  • Leveraged Android's ExifInterface API for bulletproof metadata handling.

Test Drive

  1. Basic EXIF Check

    • Snap a pic with GPS turned on.
    • Save the form.
    • Check that all EXIF data is intact using your favorite EXIF viewer.
  2. Scaled Image Test

    • Take a high-res image.
    • Verify the EXIF data after it gets scaled down.
  3. Encrypted Image Test

    • Enable media encryption in settings.
    • Capture a GPS-tagged image.
    • Confirm EXIF data survived the encryption.
  4. Edge Cases

    • Use images with no EXIF data.
    • Test images from various sources (camera, gallery, etc.).
    • Try different formats (JPG, PNG).

Tools to Verify

  • Command line nerds: Use exiftool to peek at the EXIF data.

Additional Notes

  • Full backward compatibility with existing image operations.
  • No database or form schema changes needed.
  • Minimal performance impact, as EXIF operations run only on image files.

/claim #2689

@damagatchi
Copy link

Can one of the admins verify this patch?

Copy link

algora-pbc bot commented Nov 24, 2024

Thank you for the submission!

Please ensure you meet the eligibility criteria for our program.

📝 Our team will review and process the payment if everything looks good.

Copy link
Contributor

@shubham1g5 shubham1g5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @melmathari. Your attempts looks great. Made some clarifying comments, but also wondering if it's possible to add unit tests for the new FileUtil methods you have added to copy the exif data. Thanks!

app/src/org/commcare/utils/FileUtil.java Show resolved Hide resolved
ExifInterface destExif = new ExifInterface(destFile.getAbsolutePath());

// Copy GPS data
String[] tagsToPreserve = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think we should also include these tags here -

TAG_COPYRIGHT,
TAG_IMAGE_DESCRIPTION,
TAG_DATETIME_DIGITIZED,
TAG_DATETIME_ORIGINAL,
TAG_EXIF_VERSION,
TAG_OFFSET_TIME
,TAG_OFFSET_TIME_ORIGINAL
,TAG_OFFSET_TIME_DIGITIZED,
TAG_GPS_ALTITUDE,
TAG_GPS_ALTITUDE_REF,
TAG_GPS_AREA_INFORMATION

Comment on lines 873 to 889
ExifInterface.TAG_GPS_LATITUDE_REF,
ExifInterface.TAG_GPS_LONGITUDE,
ExifInterface.TAG_GPS_LONGITUDE_REF,
ExifInterface.TAG_GPS_TIMESTAMP,
ExifInterface.TAG_GPS_DATESTAMP,
// Preserve other important EXIF data
ExifInterface.TAG_DATETIME,
ExifInterface.TAG_ORIENTATION
};

for (String tag : tagsToPreserve) {
String value = sourceExif.getAttribute(tag);
if (value != null) {
destExif.setAttribute(tag, value);
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we are not directly using the method copyExifData here ?

return scaled;
}

private static void copyExifData(ExifInterface source, ExifInterface dest) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about directly passing source and destination file paths to this method ?

@shubham1g5
Copy link
Contributor

@damagatchi ok to test

@melmathari
Copy link
Author

@shubham1g5 I've addressed the feedback. As for the unit tests, I'm not sure if I'll be able to complete them, i ran into several issues with testImplementation project(path: ':commcare-core', configuration: 'testsAsJar')

Copy link
Contributor

@shubham1g5 shubham1g5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@melmathari It's fine to ignore the lint errors in the code you have not changed as the method refactoring will need to be replicated elsewhere in the calling code.

@@ -121,7 +121,7 @@ public static boolean cleanFilePath(String fullPath, String extendedPath) {

private static final String illegalChars = "'*','+'~|<> !?:./\\";

public static String SanitizeFileName(String input) {
public static String sanitizeFileName(String input) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

think you can ignore lint errors in the code you have not changed as method renames will need to be replicated while calling the method as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry if I wasn't clear but the file name will need to be reverted here, as otherwise the project fails to compile.

@@ -356,18 +356,17 @@ public static String getGlobalStringUri(String fileLocation) {
return "file://" + fileLocation;
}

public static void checkReferenceURI(Resource r, String URI, Vector<MissingMediaException> problems) throws InvalidReferenceException {
Reference mRef = ReferenceManager.instance().DeriveReference(URI);
public static Reference checkReferenceUri(String uri) throws InvalidReferenceException {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is causing the build to fail now due to method calls not having the corresponding change, you can ignore lint errors here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will need to be reverted.

@shubham1g5
Copy link
Contributor

As for the unit tests, I'm not sure if I'll be able to complete them, i ran into several issues with testImplementation project(path: ':commcare-core', configuration: 'testsAsJar')

Curious what kind of errors are you facing here and if you have followed the instructions given here to run tests.

@melmathari
Copy link
Author

@shubham1g5 Thanks for providing the doc. Unfortunately, I won't be able to commit to the unit testing for this build due to my current schedule. We have tested this on our local implementation though.

app/lint.xml Outdated
Comment on lines 6 to 7
<ignore path="src/org/commcare/utils/FileUtil.java" />
<ignore path="src/org/commcare/activities/components/ImageCaptureProcessing.java" />
Copy link
Contributor

@shubham1g5 shubham1g5 Nov 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I didn't mean to turn off lint checks for these files, but just to ignore any errors that you have not changed (lint errors doesn't block the merge/approval on PR for us) .

@@ -121,7 +121,7 @@ public static boolean cleanFilePath(String fullPath, String extendedPath) {

private static final String illegalChars = "'*','+'~|<> !?:./\\";

public static String SanitizeFileName(String input) {
public static String sanitizeFileName(String input) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry if I wasn't clear but the file name will need to be reverted here, as otherwise the project fails to compile.

@@ -356,18 +356,17 @@ public static String getGlobalStringUri(String fileLocation) {
return "file://" + fileLocation;
}

public static void checkReferenceURI(Resource r, String URI, Vector<MissingMediaException> problems) throws InvalidReferenceException {
Reference mRef = ReferenceManager.instance().DeriveReference(URI);
public static Reference checkReferenceUri(String uri) throws InvalidReferenceException {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will need to be reverted.

@melmathari
Copy link
Author

@shubham1g5 Why did the developer bring a lint roller to the code review? Because we used SuppressLint to silence those pesky errors — nothing escapes our lint-taming prowess!

Copy link
Contributor

@shubham1g5 shubham1g5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@melmathari Indeed lol. Although what I have been trying to say is don't worry about the lint check and let it fail. No roller needed 🤣

app/src/org/commcare/utils/FileUtil.java Outdated Show resolved Hide resolved
app/src/org/commcare/utils/FileUtil.java Outdated Show resolved Hide resolved
Change requests

change request

SuppressLint

SuppressLint imageCaptureProcessing.java
@shubham1g5
Copy link
Contributor

@melmathari We are seeing instrumentation test failures on this PR which looks related to media handling.

Screenshot 2024-11-26 at 11 26 23 AM

You should be able to run these tests locally to look into the failures.

@melmathari
Copy link
Author

@shubham1g5 Thanks, I will take some time to test this a bit more locally

Copy link
Contributor

@shubham1g5 shubham1g5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious if the error with tests was due to us trying to copy the exif data from non image files ?

app/src/org/commcare/utils/FileUtil.java Outdated Show resolved Hide resolved
The file isn't an image (mimeType check)
The image doesn't have EXIF data (ExifInterface handles this gracefully)
There are any issues reading/writing EXIF data (caught and logged)
The only failure that would propagate up is if the initial file copy fails, which is appropriate since that's the core operation we need to succeed.
@melmathari
Copy link
Author

Curious if the error with tests was due to us trying to copy the exif data from non image files ?

Correct, this was the case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants